package com.anji.hyperneat.nd;

import java.util.Iterator;

import com.anji.hyperneat.nd.NDFloatArray.MatrixIterator;
import com.anji.hyperneat.onlinereinforcement.RandomGridNet;

public class NDActivatorArray {
	// The data
		private ActivatorND m[];
		
		// Contains the size of each dimension; dimension.length = # of dimensions
		private final int[] dimensions;
		
		/* The pre-calculated "area" for counting by a dimension as it maps to a single dimensional array;
		 * e.g., for an XY space with D0=X and D1=Y, asize[0] = 1 (counting by x's)  and asize[1] = MaxVal(X).
		 */
		private final int[] asize;
		
		/**
		 * Creates an N-Dimensional Float matrix with the specified dimensions.
		 * @param dimensions A list or array specifying the size of each desired dimension;
		 * e.g., new NDActivatorArray(5, 5) creates a 2D matrix with length and width = 5
		 */
		public NDActivatorArray(int... dimensions) {
			this.dimensions = dimensions;
			this.asize = new int[dimensions.length];
			
			int size = dimensions[0];
			asize[0] = 1;
			for (int i = 1; i < dimensions.length; i++) {
				size *= dimensions[i];
				asize[i] = asize[i-1] * dimensions[i-1];
			}
			m = new ActivatorND[size];
		}
		
		/**
		 * Creates an N-Dimensional Float matrix with the specified dimensions.
		 * @param dimensionsSet A list OF arrays specifying the size of each desired dimension;
		 * e.g., new NDActivatorArray({5,5}, {6,6}, {7,7,8}) creates a 7D matrix 5x5x6x6x7x7x8
		 */
		public NDActivatorArray(int[]... dimensionsSet) {
			int dimCount = 0;
			for (int dimSet = 0; dimSet < dimensionsSet.length; dimSet++) {
				dimCount += dimensionsSet[dimSet].length;
			}
			
			this.dimensions = new int[dimCount];
			dimCount = 0;
			
			for (int dimSet = 0; dimSet < dimensionsSet.length; dimSet++) {
				for (int dim = 0; dim < dimensionsSet[dimSet].length; dim++) {
					this.dimensions[dimCount++] = dimensionsSet[dimSet][dim];
				}
			}
			
			this.asize = new int[dimensions.length];
			
			int size = dimensions[0];
			asize[0] = 1;
			for (int i = 1; i < dimensions.length; i++) {
				size *= dimensions[i];
				asize[i] = asize[i-1] * dimensions[i-1];
			}
			m = new ActivatorND[size];
		}
		
		/**
		 * Creates an N-Dimensional Activator array with the specified dimensions, with each set cast to castSize
		 * e.g., new NDFloatMatrix(3, {5,5}, {6,6}, {7,7,8}) creates a 9D array 5x5x1x6x6x1x7x7x8
		 * @param castSize The number of dimensions to cast each dimension set to.
		 * @param dimensionsSet A list of arrays specifying the size of each desired dimension.
		 */
		public NDActivatorArray(int castSize, int[]... dimensionsSet) {
			int dimCount;
			
			this.dimensions = new int[castSize * dimensionsSet.length];
			dimCount = 0;
			
			for (int dimSet = 0; dimSet < dimensionsSet.length; dimSet++) {
				for (int dim = 0; dim < castSize; dim++) {
					// Are we in the bounds of the originally specified dimension?
					if (dim < dimensionsSet[dimSet].length) {
						// If so, go ahead and use the index
						this.dimensions[dimCount++] = dimensionsSet[dimSet][dim];	
					} else {
						// otherwise, cast to size one
						this.dimensions[dimCount++] = 1;
					}
				}
			}
			
			this.asize = new int[dimensions.length];
			
			int size = dimensions[0];
			asize[0] = 1;
			for (int i = 1; i < dimensions.length; i++) {
				size *= dimensions[i];
				asize[i] = asize[i-1] * dimensions[i-1];
			}
			m = new ActivatorND[size];
		}
		
		/**
		 * Private constructor for cloning.
		 * @param dimensions
		 * @param asize
		 * @param m
		 */
		private NDActivatorArray(int[] dimensions, int[] asize, ActivatorND[] m){
			this.dimensions = dimensions;
			this.asize = asize;
			this.m = m;
		}
		
		/**
		 * Sets all values in the array to 0. 
		 */
		public void clear() {
			for (int i = 0; i < m.length; i++) {
				m[i] = null;
			}
		}
		
		/**
		 * Compares the dimensions of this Array to another; returns true if they match, false otherwise.
		 * @param other The other array to compare against.
		 * @return
		 */
		public boolean dimensionsMatch(NDActivatorArray other) {
			if (this.dimensions.length != other.dimensions.length)	return false;
			for (int i = 0; i < dimensions.length; i++) {
				if (this.dimensions[i] != other.dimensions[i]) return false;
			}
			return true;
		}
		
		
		/**
		 * Retrieves the float at the point specified by the coordinates;
		 * the number of coordinates must match the number of dimensions, or an
		 * exception will be thrown.
		 * E.g., get(3, 4, 5) retrieves the point of those coordinates in a 3D matrix. 
		 * @param coords The list or array of coordinates for the desired point.
		 * @return The value at the specified point in the matrix.
		 */
		public ActivatorND get(int... coords) {
			int idx = 0;
			for (int d = dimensions.length-1; d >= 0; d--) {
				idx += asize[d] * coords[d];
			}
			return m[idx];
		}
		
		/**
		 * Sets the float value at the point specified by the coordinates;
		 * the number of coordinates must match the number of dimensions, or an
		 * exception will be thrown.
		 * E.g., get(5.0, 3, 4, 5) sets the point (3,4,5) in a 3D matrix to a value of 5.0.
		 * @param activator The value to set. 
		 * @param coords The list or array of coordinates for the desired point.
		 */
		public void set(ActivatorND activator, int... coords) {
			int idx = 0;
			for (int d = dimensions.length-1; d >= 0; d--) {
				idx += asize[d] * coords[d];
			}
			
			m[idx] = activator;
		}
		
		/**
		 * @return The number of dimensions this array has.
		 */
		public int getNumDimensions() {return dimensions.length;}
		
		/**
		 * @param dimension The dimension for which you wish to retrieve the size.
		 * @return The size of the specified dimension.
		 */
		public int getSizeOfDimension(int dimension) {return dimensions[dimension];}
		
		/**
		 * @return An int array describing the dimensions of this Array.
		 */
		public int[] getDimensions() {return dimensions;}
		
		/**
		 * Convenience method for returning the dimensions as a string in the format of #x#x#, 
		 * e.g., 
		 * 5x5x1
		 * @return The string representing the dimensions.
		 */
		public String getDimensionString() {
			StringBuilder sb = new StringBuilder();
			for (int i = 0; i < dimensions.length - 1; i++) {
				sb.append(dimensions[i]);
				sb.append("x");
			}
			sb.append(dimensions[dimensions.length-1]);
			
			return sb.toString();
		}
		
		/**
		 * @return The total size of the array.
		 */
		public int size() {return m.length;}
		
		/**
		 * Returns an iterator for the array;
		 * if you are stepping through the entire array, using the iterator will
		 * be faster than iterating using get().
		 * @return An array iterator.
		 */
		public ActivatorArrayIterator iterator() {return new ActivatorArrayIterator();}
		
		/**
		 * Retrives the coordinates of the element in this Array with the highest value.
		 * @return Coordinates of max value element.
		 */
//	    public int[] getCoordinatesOfMaxValue() {
//	        int[] coords = null;
//	        float best = -1;
//	        
//	        for (MatrixIterator it = this.iterator(); it.hasNext(); it.next()) {
//	        	if (null == coords || it.get() > best) {
//	        		best = it.get();
//	        		coords = it.getCurrentCoordinates();
//	        	}
//	        }
//	        return coords;
//	    }
		
	    /**
	     * Convenience method to retrieve a complete set of coordinates from two sets of coordinates.
	     * @param coords1 Set of coordinates for the first node
	     * @param coords2 set of coordinates for the second node
	     * @return
	     */
		public ActivatorND get(int[] coords1, int[] coords2) {
			int[] coords = new int[coords1.length + coords2.length];
			for(int i = 0; i < coords1.length; i++) coords[i] = coords1[i];
			for(int i = 0; i < coords2.length; i++) coords[i+coords1.length] = coords2[i];
			return get(coords);
		}
		
	    /**
	     * Creates a clone of this Array.
	     */
	    public NDActivatorArray clone() {
	    	int[] odimensions = new int[dimensions.length];
	    	System.arraycopy(dimensions, 0, odimensions, 0, dimensions.length);
	    	
	    	int[] oasize = new int[asize.length];
	    	System.arraycopy(asize, 0, oasize, 0, asize.length);
	    	
	    	ActivatorND[] om = new ActivatorND[m.length]; 
	    	System.arraycopy(m, 0, om, 0, m.length);

	    	NDActivatorArray oarray = new NDActivatorArray(odimensions, oasize, om);
	    	return oarray;
	    }
	    
		/**
		 * Builds a string representation of this array.
		 */
		public String toString() {
			StringBuilder sb = new StringBuilder();
			if (dimensions.length == 1) {
				for (int i = 0; i < m.length; i++) sb.append(String.valueOf(m[i]) + " ");
			} else if (dimensions.length == 2) {
				int idx = 0; 
				while (idx < m.length) {
					sb.append(String.valueOf(m[idx++]) + " ");
					if (idx % dimensions[0] == 0) sb.append("\n"); 
						
				}
			} else {
				// On a 2d screen, this is about the best we can do:
				int idx = 0; 
				while (idx < m.length) {
					sb.append(String.valueOf(m[idx++]) + " ");
					if (idx % dimensions[0] == 0) sb.append("\n"); 
					if (idx % (dimensions[0] * dimensions[1]) == 0) sb.append("\n");
				}
			}
			return sb.toString();
		}

		/**
		 * FOR TESTING ONLY
		 * @param args
		 */
		public static void main(String[] args) {
			NDActivatorArray ndas = new NDActivatorArray(new int[] {2,2,2});
			
			RandomGridNet net;
			
			net = RandomGridNet.getRandomGridNet(2, new int[][] {{2,2}, {2,2}});
			ndas.set(net, 0,0,0);
			net = RandomGridNet.getRandomGridNet(2, new int[][] {{2,2}, {2,2}});
			ndas.set(net, 0,0,1);
			net = RandomGridNet.getRandomGridNet(2, new int[][] {{2,2}, {2,2}});
			ndas.set(net, 0,1,0);
			net = RandomGridNet.getRandomGridNet(2, new int[][] {{2,2}, {2,2}});
			ndas.set(net, 0,1,1);
			
			net = RandomGridNet.getRandomGridNet(2, new int[][] {{2,2}, {2,2}});
			ndas.set(net, 1,0,0);
			net = RandomGridNet.getRandomGridNet(2, new int[][] {{2,2}, {2,2}});
			ndas.set(net, 1,0,1);
			net = RandomGridNet.getRandomGridNet(2, new int[][] {{2,2}, {2,2}});
			ndas.set(net, 1,1,0);
			net = RandomGridNet.getRandomGridNet(2, new int[][] {{2,2}, {2,2}});
			ndas.set(net, 1,1,1);
			
			System.out.println("TestArray:");
			System.out.println(ndas);
		}
		
		/**
		 * A simple iterator for the array;
		 * if you are stepping through the entire array, using the iterator will
		 * be faster than iterating using get().
		 *
		 */
		public class ActivatorArrayIterator implements Iterator {
			private int idx;
			
			private ActivatorArrayIterator() {idx = 0;}
			
			@Override
			public boolean hasNext() {return idx < m.length;}

			@Override
			public ActivatorND next() {return m[idx++];}

			@Override
			public void remove() {/* no-op*/}	
			
			public int[] getCurrentCoordinates() {
				int[] c = new int[dimensions.length];
				int localidx = idx;
				for (int dim = dimensions.length-1; dim >=0 ; dim--) {
					c[dim] = localidx / asize[dim];
					localidx -= asize[dim] * c[dim] ;
				}
				return c;
			}
			
			public void set(ActivatorND activator) {m[idx] = activator;}
			public ActivatorND get() {return m[idx];}
			public void reset() {idx = 0;}
		}

	}

